
//
// Copyright (c) 2008, the /*jupiter jazz*/ group
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//     * Redistributions of source code must retain the above copyright
//       notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//     * Neither the name of the /*jupiter jazz*/ group nor the
//       names of its contributors may be used to endorse or promote products
//       derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY the /*jupiter jazz*/ group ''AS IS'' AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL the /*jupiter jazz*/ group BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
//

// Interface header.
#include "progressiveexrimagefilereader.h"

// classlib headers.
#include "classlib/canvasproperties.h"
#include "classlib/stlext.h"
#include "classlib/tile.h"

// OpenEXR headers.
#include "openexr/ImathBox.h"
#include "openexr/ImfChannelList.h"
#include "openexr/ImfFrameBuffer.h"
#include "openexr/ImfHeader.h"
#include "openexr/ImfInputFile.h"
#include "openexr/ImfPixelType.h"
#include "openexr/ImfTileDescription.h"
#include "openexr/ImfTiledInputFile.h"

// Standard headers.
#include <algorithm>
#include <cassert>
#include <cstring>
#include <memory>
#include <vector>

using namespace Iex;
using namespace Imath;
using namespace Imf;
using namespace std;

namespace foundation
{

//
// ProgressiveEXRImageFileReader class implementation.
//

struct ProgressiveEXRImageFileReader::Impl
{
    size_t                      m_default_tile_width;
    size_t                      m_default_tile_height;
    auto_ptr<InputFile>         m_scanline_file;
    auto_ptr<TiledInputFile>    m_tiled_file;
    bool                        m_is_tiled;
    const Channel*              m_red;
    const Channel*              m_green;
    const Channel*              m_blue;
    const Channel*              m_alpha;
    Box2i                       m_dw;
    CanvasProperties            m_props;
    vector<unsigned char>       m_scanlines;
    size_t                      m_last_tile_y;
};

// Constructors.
ProgressiveEXRImageFileReader::ProgressiveEXRImageFileReader()
  : impl(new Impl())
{
    impl->m_default_tile_width = 64;
    impl->m_default_tile_height = 16;
}
ProgressiveEXRImageFileReader::ProgressiveEXRImageFileReader(
    const size_t        default_tile_width,
    const size_t        default_tile_height)
  : impl(new Impl())
{
    impl->m_default_tile_width = default_tile_width;
    impl->m_default_tile_height = default_tile_height;
}

// Destructor.
ProgressiveEXRImageFileReader::~ProgressiveEXRImageFileReader()
{
    if (is_open())
        close();

    delete impl;
}

// Open an OpenEXR image file.
void ProgressiveEXRImageFileReader::open(const char* filename)
{
    assert(filename);
    assert(!is_open());

    try
    {
        try
        {
            // Open the file for reading, assuming the file is tiled.
            impl->m_tiled_file.reset(new TiledInputFile(filename));
            impl->m_is_tiled = true;
        }
        catch (const ArgExc&)
        {
            // The file is not tiled, open it as a scanline file.
            impl->m_scanline_file.reset(new InputFile(filename));
            impl->m_is_tiled = false;
        }

        // Retrieve the file header, and perform basic sanity checks on it.
        const Header& header = impl->m_is_tiled
            ? impl->m_tiled_file->header()
            : impl->m_scanline_file->header();
        header.sanityCheck(impl->m_is_tiled);

        // Retrieve the image channels.
        const ChannelList& channels = header.channels();
        impl->m_red = channels.findChannel("R");
        impl->m_green = channels.findChannel("G");
        impl->m_blue = channels.findChannel("B");
        impl->m_alpha = channels.findChannel("A");
        if (!(impl->m_red && impl->m_green && impl->m_blue))
            throw ExceptionUnsupportedImageFormat();

        // Make sure all channels use the same pixel format.
        if (!(impl->m_red->type == impl->m_green->type && impl->m_red->type == impl->m_blue->type))
            throw ExceptionUnsupportedImageFormat();
        if (impl->m_alpha)
        {
            if (impl->m_red->type != impl->m_alpha->type)
                throw ExceptionUnsupportedImageFormat();
        }

        // Figure out the pixel format, based on the pixel type of the file.
        PixelFormat pixel_format = PixelFormatFloat;
        switch (impl->m_red->type)
        {
          case UINT: pixel_format = PixelFormatUInt32; break;
          case HALF: pixel_format = PixelFormatHalf; break;
          case FLOAT: pixel_format = PixelFormatFloat; break;
          default: throw ExceptionUnsupportedImageFormat();
        }

        // Retrieve the dimensions of the image.
        impl->m_dw = header.dataWindow();

        // Retrieve the dimensions of the tiles.
        size_t tile_width, tile_height;
        if (impl->m_is_tiled)
        {
            assert(header.hasTileDescription());
            const TileDescription& td = header.tileDescription();
            tile_width = static_cast<size_t>(td.xSize);
            tile_height = static_cast<size_t>(td.ySize);
        }
        else
        {
            tile_width = impl->m_default_tile_width;
            tile_height = impl->m_default_tile_height;
        }

        // Retrieve the number of tiles in each direction.
        size_t tile_count_x, tile_count_y;
        if (impl->m_is_tiled)
        {
            tile_count_x = static_cast<size_t>(impl->m_tiled_file->numXTiles());
            tile_count_y = static_cast<size_t>(impl->m_tiled_file->numYTiles());
        }
        else
        {
            const double nx = ceil(static_cast<double>(impl->m_dw.max.x - impl->m_dw.min.x) / tile_width);
            const double ny = ceil(static_cast<double>(impl->m_dw.max.y - impl->m_dw.min.y) / tile_height);
            tile_count_x = static_cast<size_t>(nx);
            tile_count_y = static_cast<size_t>(ny);
        }

        // Set canvas properties.
        impl->m_props.m_canvas_width = static_cast<size_t>(impl->m_dw.max.x - impl->m_dw.min.x + 1);
        impl->m_props.m_canvas_height = static_cast<size_t>(impl->m_dw.max.y - impl->m_dw.min.y + 1);
        impl->m_props.m_inv_canvas_width = 1.0 / impl->m_props.m_canvas_width;
        impl->m_props.m_inv_canvas_height = 1.0 / impl->m_props.m_canvas_height;
        impl->m_props.m_pixel_count = impl->m_props.m_canvas_width * impl->m_props.m_canvas_height;
        impl->m_props.m_tile_width = tile_width;
        impl->m_props.m_tile_height = tile_height;
        impl->m_props.m_inv_tile_width = 1.0 / impl->m_props.m_tile_width;
        impl->m_props.m_inv_tile_height = 1.0 / impl->m_props.m_tile_height;
        impl->m_props.m_tile_count_x = tile_count_x;
        impl->m_props.m_tile_count_y = tile_count_y;
        impl->m_props.m_tile_count = impl->m_props.m_tile_count_x * impl->m_props.m_tile_count_y;    
        impl->m_props.m_pixel_format = pixel_format;
        impl->m_props.m_channel_count = impl->m_alpha ? 4 : 3;
        impl->m_props.m_pixel_size = impl->m_props.m_channel_count * pixel_size(impl->m_props.m_pixel_format);

        // Allocate memory to store scanlines, if the file is not tiled.
        if (!impl->m_is_tiled)
        {
            const size_t size =
                  impl->m_props.m_canvas_width
                * impl->m_props.m_pixel_size
                * tile_height;
            impl->m_scanlines.resize(size);
        }

        // No tile previously accessed.
        impl->m_last_tile_y = ~size_t(0);
    }
    catch (const BaseExc& e)
    {
        // I/O error.
        throw ExceptionIOError(e.what());
    }
}

// Close the image file.
void ProgressiveEXRImageFileReader::close()
{
    assert(is_open());

    impl->m_scanline_file.reset();
    impl->m_tiled_file.reset();

    clear_release_memory(impl->m_scanlines);
}

// Return true if an image file is currently open.
bool ProgressiveEXRImageFileReader::is_open() const
{
    return impl->m_is_tiled
        ? impl->m_tiled_file.get() != 0
        : impl->m_scanline_file.get() != 0;
}

// Read canvas properties.
void ProgressiveEXRImageFileReader::read_canvas_properties(
    CanvasProperties&   props)
{
    assert(is_open());
    props = impl->m_props;
}

// Read an image tile. Returns a newly allocated tile.
Tile* ProgressiveEXRImageFileReader::read_tile(
    const size_t        tile_x,
    const size_t        tile_y)
{
    assert(is_open());

    try
    {
        const int ix = static_cast<int>(tile_x);
        const int iy = static_cast<int>(tile_y);

        // Retrieve the data window of the tile.
        Box2i range;
        if (impl->m_is_tiled)
        {
            range = impl->m_tiled_file->dataWindowForTile(ix, iy);
        }
        else
        {
            const int tw = static_cast<int>(impl->m_props.m_tile_width);
            const int th = static_cast<int>(impl->m_props.m_tile_height);
            range.min.x = impl->m_dw.min.x + ix * tw;
            range.min.y = impl->m_dw.min.y + iy * th;
            range.max.x = std::min(range.min.x + tw - 1, impl->m_dw.max.x);
            range.max.y = std::min(range.min.y + th - 1, impl->m_dw.max.y);
        }

        // Compute the dimensions of the tile.
        const size_t tile_width = range.max.x - range.min.x + 1;
        const size_t tile_height = range.max.y - range.min.y + 1;

        // Create a new tile.
        Tile* tile =
            new Tile(
                impl->m_props.m_pixel_format,
                tile_width,
                tile_height,
                impl->m_props.m_channel_count);

        const size_t channel_size = pixel_size(impl->m_props.m_pixel_format);
        const size_t stride_x = impl->m_props.m_pixel_size;
        const size_t stride_y = impl->m_is_tiled
            ? stride_x * tile_width
            : stride_x * impl->m_props.m_canvas_width;
        const size_t origin = impl->m_is_tiled
            ? range.min.y * stride_y + range.min.x * stride_x
            : range.min.y * stride_y;
        const char* base = impl->m_is_tiled
            ? reinterpret_cast<const char*>(tile->pixel(0, 0)) - origin
            : reinterpret_cast<const char*>(&impl->m_scanlines[0]) - origin;

        // Construct the FrameBuffer object.
        FrameBuffer framebuffer;
        framebuffer.insert(
            "R",
            Slice(
                impl->m_red->type,
                const_cast<char*>(base + 0 * channel_size),
                stride_x,
                stride_y));
        framebuffer.insert(
            "G",
            Slice(
                impl->m_green->type,
                const_cast<char*>(base + 1 * channel_size),
                stride_x,
                stride_y));
        framebuffer.insert(
            "B",
            Slice(
                impl->m_blue->type,
                const_cast<char*>(base + 2 * channel_size),
                stride_x,
                stride_y));
        if (impl->m_alpha)
        {
            framebuffer.insert(
                "A",
                Slice(
                    impl->m_alpha->type,
                    const_cast<char*>(base + 3 * channel_size),
                    stride_x,
                    stride_y));
        }

        if (impl->m_is_tiled)
        {
            // Read the tile.
            impl->m_tiled_file->setFrameBuffer(framebuffer);
            impl->m_tiled_file->readTile(ix, iy);
        }
        else
        {
            if (impl->m_last_tile_y != tile_y)
            {
                // Read the scanlines intersecting the tile.
                impl->m_scanline_file->setFrameBuffer(framebuffer);
                impl->m_scanline_file->readPixels(range.min.y, range.max.y);
                impl->m_last_tile_y = tile_y;
            }

            // Extract the tile from the scanlines.
            for (size_t y = 0; y < tile_height; ++y)
            {
                const size_t index = (impl->m_props.m_canvas_width * y + range.min.x) * stride_x;
                memcpy(
                    tile->pixel(0, y),
                    &impl->m_scanlines[index],
                    tile_width * stride_x);
            }
        }

        return tile;
    }
    catch (const BaseExc& e)
    {
        // I/O error.
        throw ExceptionIOError(e.what());
    }
}

}   // namespace foundation
